/*
 * Copyright (C) 2005 Johan Maasing johan at zoom.nu Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
 * or agreed to in writing, software distributed under the License is
 * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */
package nu.zoom.swing.desktop.worker;

import java.awt.EventQueue;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * Invocation handler that looks for a Worker policy annotation on the called
 * method to determine if the method should be called in the callers thread or
 * run in another thread.
 * 
 * @author Johan Maasing
 * 
 */
public class WorkerInvocationHandler implements InvocationHandler {

    private Log log = LogFactory.getLog(getClass());
    private Object instance;
    private ThreadGroup threadGroup = new ThreadGroup("Worker threads");

    public WorkerInvocationHandler(Object instance) {
        super();
        this.instance = instance;
    }

    @Override
    public Object invoke(final Object proxy, final Method method,
            final Object[] args) throws Throwable {
        EventQueuePolicy policy = method.getAnnotation(EventQueuePolicy.class);
        if (policy == null) {
            return method.invoke(instance, args);
        } else {
            if (log.isTraceEnabled()) {
                log.trace("Event queue policy detected for method: " + method.getName());
            }
            boolean noDelayedResult = (void.class.equals(method.getReturnType()) && (method.getExceptionTypes().length == 0));
            switch (policy.value()) {
                case EVENT_QUEUE:
                    if (EventQueue.isDispatchThread()) {
                        return method.invoke(instance, args);
                    } else {
                        log.trace("Dispatching method call to EventQueue, wait for result: " + noDelayedResult);
                        if (noDelayedResult) {
                            // We do not have to wait for a result. Run and return
                            // immediatly.
                            EventQueue.invokeLater(new Runnable() {

                                @Override
                                public void run() {
                                    try {
                                        method.invoke(instance, args);
                                    } catch (Exception e) {
                                        throw new IllegalStateException(
                                                "Worker thread method threw undeclared exception",
                                                e);
                                    }
                                }
                            });
                            return null;
                        } else {
                            // Run and block until a result is ready
                            DelayedResultWorker worker = new DelayedResultWorker(
                                    instance, method, args);
                            invokeAndWait(worker, true);
                            if (worker.exception != null) {
                                throw worker.exception;
                            } else {
                                return worker.result;
                            }
                        }
                    }
                // break;
                case NOT_EVENT_QUEUE:
                    if (EventQueue.isDispatchThread()) {
                        log.trace("Dispatching method call to new thread, wait for result: " + noDelayedResult);
                        if (noDelayedResult) {
                            // We do not have to wait for a result. Run and return
                            // immediatly.
                            Thread t = new Thread(threadGroup, new Runnable() {

                                @Override
                                public void run() {
                                    try {
                                        method.invoke(instance, args);
                                    } catch (Exception e) {
                                        throw new IllegalStateException(
                                                "Worker thread method threw undeclared exception",
                                                e);
                                    }
                                }
                            });
                            t.start();
                            return null;
                        } else {
                            // Run and block until a result is ready
                            DelayedResultWorker worker = new DelayedResultWorker(
                                    instance, method, args);
                            invokeAndWait(worker, false);
                            if (worker.exception != null) {
                                throw worker.exception;
                            } else {
                                return worker.result;
                            }
                        }
                    } else {
                        return method.invoke(instance, args);
                    }
                // break;
                default:
                    // This only happens if we extend the policy but forget to add a
                    // strategy for it
                    return method.invoke(instance, args);
            }
        }
    }

    private void invokeAndWait(DelayedResultWorker worker, boolean eventQueue)
            throws InterruptedException {
        if (eventQueue) {
            EventQueue.invokeLater(worker);
        } else {
            Thread t = new Thread(threadGroup, worker);
            t.start();
        }
        worker.waitForResult();
    }

    class DelayedResultWorker implements Runnable {

        private Object result = null;
        private Exception exception = null;
        private Object instance;
        private Method method;
        private Object[] args;

        DelayedResultWorker(final Object instance, final Method method,
                final Object[] args) {
            this.instance = instance;
            this.method = method;
            this.args = args;
        }

        synchronized void setResult(Object result) {
            this.result = result;
        }

        synchronized void setException(Exception e) {
            if ((e instanceof InvocationTargetException)
                    && (e.getCause() != null)
                    && (e.getCause() instanceof Exception)) {
                this.exception = (Exception) e.getCause();
            } else {
                this.exception = e;
            }
        }

        synchronized boolean isResult() {
            return ((result != null) || (exception != null));
        }

        synchronized void waitForResult() {
            log.trace("Waiting for worker result");
            while (!isResult()) {
                try {
                    wait();
                } catch (InterruptedException e) {
                    setException(e);
                }
            }
            log.trace("Worker result obtained");
        }

        @Override
        public synchronized void run() {
            try {
                Object resultObject = method.invoke(instance, args);
                setResult(resultObject);
            } catch (Exception e) {
                setException(e);
            } finally {
                notifyAll();
            }
        }
    }

    @Override
    public String toString() {
        return "Event queue interceptor for : " + instance.getClass().getName();
    }
}
